package cn.com.zero.api.log.aspect;

import cn.com.zero.api.log.annotation.Log;
import cn.com.zero.api.log.dubbo.ILogService;
import cn.com.zero.api.log.util.LogUtil;
import cn.com.zero.api.log.vo.LogVO;
import cn.com.zero.base.config.ReturnCodeConfig;
import cn.com.zero.base.exception.ErrorCodeException;
import cn.com.zero.web.util.HttpRequestUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.PropertyFilter;
import org.apache.commons.io.IOUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author xiongxj
 * @version 1.0.0
 * @Description 日志切面类
 * @createTime 2022/7/13 14:11
 */
@Aspect
@Component
@Order(101)
public class LogAspect {
    /**
     * 数据库 varchar 类型最大长度，备注信息、日志内容的最大长度
     * 如果超过本长度需要将数据库调整成 text 或将备注信息截取
     */
    private static final int VARCHAR_MAX_LENGTH = 255;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private ILogService logService;

    @Value("${spring.application.name}")
    private String moduleName;

    /**
     * 返回码配置
     */
    private ReturnCodeConfig returnCodeConfig;

    /**
     * 拦截所有被 @Log 注解的访问请求
     */
    @Pointcut("@annotation(cn.com.zero.api.log.annotation.Log)")
    public void pointcut() {
    }

    /**
     * 定义拦截后执行的方法<br/>
     * 注解 @Around 定义拦截点，这个是拦截的固定写法
     *
     * @param joinPoint 拦截业务传入参数，包括访问方法等
     * @return 返回对象即为被拦截的方法返回的对象
     * @throws Throwable 抛出所有异常
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        LogVO logVO = new LogVO();

        // 设置基本数据
        this.setLogPO(logVO, joinPoint);

        // 设置拦截器数据
        this.setLogPOByJoinPoint(logVO, joinPoint);

        // 返回结果
        Object result = null;
        // 异常信息
        Exception exception = null;
        long beginTime = System.currentTimeMillis();
        // 执行方法
        try {
            logger.trace("开始执行请求：{}", logVO.toString());

            result = joinPoint.proceed();

            logVO.setStatus(true);

            // 设置ThreadLocal数据
            logVO.setRemark(this.subString(LogUtil.getNote()));
            logVO.setResponseContent(this.subString(JSON.toJSONString(result)));

            logger.trace("请求执行成功：{}", logVO.toString());
        } catch (ErrorCodeException e) {
            logVO.setStatus(false);
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(e.getCode())
                    .append(":")
                    .append(returnCodeConfig.getMessage(String.valueOf(e.getCode()),
                            e.getArgs()));
            // 处理错误信息，截取长度防止超过数据库字段长度
            logVO.setErrorContent(this.subString(stringBuilder.toString()));
            exception = e;

            logger.error("请求执行发生异常：{}，异常信息：{}", logVO.toString(), e);

        } catch (Exception e) {

            logVO.setStatus(false);
            // 处理错误信息，截取长度防止超过数据库字段长度
            logVO.setErrorContent(this.subString(e.getMessage()));
            exception = e;

            logger.error("请求执行发生异常：{}，异常信息：{}", logVO.toString(), e);
        } finally {
            // 执行时长(毫秒)
            long costTime = System.currentTimeMillis() - beginTime;
            logVO.setCostTime(costTime);

            // 清除ThreadLocal内存数据
            LogUtil.clearNote();

            // 日志入库
            this.saveLog(logVO, joinPoint);

            // 将异常继续抛出
            if (exception != null) {
                throw exception;
            }
        }

        return result;
    }

    /**
     * 设置日志基础数据
     *
     * @param logVO     日志对象
     * @param joinPoint 切入点
     */
    private void setLogPO(LogVO logVO, ProceedingJoinPoint joinPoint) {
        // 设置注解数据
        this.setLogPOByAnnotation(logVO, joinPoint);

        // 请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            logVO.setUserId(request.getHeader("UserId"));
            logVO.setIpAddress(HttpRequestUtil.getRequestIp());
            logVO.setRequestMethod(request.getMethod());
            logVO.setRequestUrl(request.getRequestURI());
            logVO.setRequestBody(this.getRequestBody(request, null));
            logVO.setRequestParameters(this.getRequestParameters(request, null));
        }
    }

    /**
     * 通过切点设置日志对象
     *
     * @param logVO     日志对象
     * @param joinPoint 切点
     */
    private void setLogPOByJoinPoint(LogVO logVO, JoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        logVO.setMethodName(className + "." + methodName + "()");
    }

    /**
     * 通过注解设置日志对象
     *
     * @param logVO     日志对象
     * @param joinPoint 切点
     */
    private void setLogPOByAnnotation(LogVO logVO, JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Log logAnnotation = method.getAnnotation(Log.class);
        logVO.setModuleName(moduleName);
        logVO.setContent(logAnnotation.content());
        logVO.setCatalogType(logAnnotation.logType());
    }

    /**
     * 日志入库
     *
     * @param logVO     日志对象
     * @param joinPoint 切入点
     */
    private void saveLog(LogVO logVO, JoinPoint joinPoint) {
        logService.saveLog(logVO);
    }

    /**
     * 字符串截取，防止超过数据库长度
     *
     * @param str 待截取字符串
     * @return 处理后的字符串
     */
    private String subString(String str) {
        String string = str;
        // 处理错误信息，截取长度防止超过数据库字段长度
        if (!StrUtil.isBlank(str) && str.length() > VARCHAR_MAX_LENGTH) {
            string = str.substring(0, VARCHAR_MAX_LENGTH);
        }
        return string;
    }

    /**
     * 获取请求参数
     *
     * @param request      请求对象
     * @param ignoreFields 需要忽略的字段
     * @return 请求参数字符串
     */
    private String getRequestParameters(HttpServletRequest request, String[] ignoreFields) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        Map<String, String[]> tempParameterMap = new ConcurrentHashMap<>(parameterMap.size());

        if (parameterMap.size() > 0) {
            if (ignoreFields == null || ignoreFields.length == 0) {
                tempParameterMap = parameterMap;
            } else {
                //由于 request.getParameterMap()是只读对象，所以只能遍历后放到新的对象中
                for (String key : parameterMap.keySet()) {
                    boolean isIgnore = false;
                    for (String ignoreField : ignoreFields) {
                        if (key.equalsIgnoreCase(ignoreField)) {
                            isIgnore = true;
                            break;
                        }
                    }
                    if (!isIgnore) {
                        tempParameterMap.put(key, parameterMap.get(key));
                    }
                }
            }
        }
        try {
            return JSON.toJSONString(tempParameterMap);
        } catch (Exception e) {
            return "";
        }
    }

    /**
     * 获取请求头体
     *
     * @param request      请求对象
     * @param ignoreFields 需要忽略的字段
     * @return 请求体字符串
     */
    private String getRequestBody(HttpServletRequest request, String[] ignoreFields) {
        try {
            String requestContent = IOUtils.toString(request.getReader());
            Object object = JSON.parse(requestContent);
            return this.getIgnoredContent(object, ignoreFields);
        } catch (Exception e) {
            return "";
        }
    }


    /**
     * 获取过滤后内容
     *
     * @param object       待过滤对象
     * @param ignoreFields 需要忽略的字段
     * @return 过滤后内容
     */
    private String getIgnoredContent(Object object, String[] ignoreFields) {
        try {
            if (object == null) {
                return "";
            }
            //如果是字符串类型则需要转为对象，否则不会走到过滤器
            if (object instanceof String) {
                try {
                    object = JSON.parse((String) object);
                } catch (Exception e) {
                    return (String) object;
                }
            }
            if (ignoreFields == null || ignoreFields.length == 0) {
                return JSON.toJSONString(object);
            }
            PropertyFilter filter = (source, name, value) -> {
                for (String ignoreField : ignoreFields) {
                    if (name.equalsIgnoreCase(ignoreField)) {
                        return false;
                    }
                }
                return true;
            };
            return JSON.toJSONString(object, filter);
        } catch (Exception e) {
            return "";
        }
    }

    @Autowired(required = false)
    @DubboReference
    public void setLogService(ILogService logService) {
        this.logService = logService;
    }

    @Autowired(required = false)
    public void setModuleName(String moduleName) {
        this.moduleName = moduleName;
    }

    @Autowired
    public void setReturnCodeConfig(ReturnCodeConfig returnCodeConfig) {
        this.returnCodeConfig = returnCodeConfig;
    }
}
